MoE Distribute Dispatch V2(Full-Mesh)SIMT 内核实现分析
1. 算子概述
1.1 这个算子是做什么的?
在混合专家(MoE, Mixture of Experts)大模型中,每一层有上百个"专家"(每个专家本质上是一个 FFN 子网络),分布在不同的 AI 加速卡上。每个 token(词元)只会激活其中的 Top-K 个专家。因此必须有一个通信算子,把本卡的 token "快递"到持有目标专家的那张卡上——这就是 Dispatch(分发)算子。训练/推理完成后,还有反向的 Combine(回收)算子 把专家的输出寄回来。
本文件的实现有三大特点,也是本文的主线:
| 特点 | 含义 | 对应代码位置 |
|---|---|---|
| SIMT 实现 | 不是传统的 SIMD(向量)风格,而是用 __simt_vf__ 线程函数 + 原子操作完成不规则路由(每个 token 去向不同) | L122 simt_prepare_mapping 等 |
| URMA 硬件通信 | 用 AI Core 直接生成 WQE(工作队列元素)并"敲门铃",由 URMA 引擎执行远程写,绕过 CPU/Host | L3277 simt_nw_mj_vf、L4001 RingDoorbell |
| Full-Mesh 全互联 | 每张卡和所有其他卡之间直接有通信窗(window),点对点读写,无需交换机式的环形/树形路由 | 类名 FullMesh;窗口地址函数 L462 GetWindAddrByRankId |
2. SIMT 架构基础概念
2.1 什么是 SIMT?和 SIMD 有什么不同?
SIMD(单指令多数据):一条向量指令同时处理一整块数据,编程时把数据切到 UB(Unified Buffer,统一缓存)里,用 Add、DataCopy 等 API 搬运和计算——适合规则的大块数据。
SIMT(单指令多线程):像 GPU 一样,一个 AI Core 内启动成百上千个线程,每个线程有独立的程序计数器、可独立分支,通过原子操作和栅栏协作——适合不规则、依赖数据内容的场景。Dispatch 恰恰就是典型的不规则场景:每个 token 该写到哪个槽位,只有运行时才知道。
2.2 Ascend 上的 SIMT 编程模型(本文代码用到的全部语法)
本算子只用到了下面这一套"最小语法集",先看懂它们,后面读代码就毫无障碍:
| 语法 / API | 作用 | 本算子示例 |
|---|---|---|
__simt_vf__ __aicore__ | 标记这是一个 VF(向量函数),可在核内以多线程方式启动 | L122 |
LAUNCH_BOUND(N) | 声明线程数上限,N 必须是编译期常量(本算子全是 constexpr) | LAUNCH_BOUND(PREPARE_THREADS_NUM),1024 线程,L92 |
Simt::VF_CALL<Fn>(Dim3{N}, args...) | 启动 VF:N 个线程并行执行 Fn(args...),调用线程阻塞等待全部完成 | L3721 |
Simt::GetThreadIdx<0>() | 获取线程号 tid(0 ~ N-1) | L135 |
__ubuf__ T* / __gm__ T* | 地址空间标注:__ubuf__ 指向同核所有线程共享的 UB;__gm__ 指向全局内存(可跨核跨卡) | L123 |
atomicAdd(ptr, v) | 原子加。本算子大量用它在 UB 上做"线程安全的计数器 + 取号机" | L143 |
Simt::ThreadBarrier() | 核内线程栅栏(等价 CUDA 的 __syncthreads):所有线程到齐才继续 | L151 |
atomicOr / asc_threadfence() | 原子按位或(置位图)/内存栅栏(保证写序) | L3360 |
st_dev(val, addr, 0) | 向设备侧特殊寄存器/门铃地址写值(触发 URMA 硬件) | L4262 |
ScalarGetSFFValue<mode>(x) | 位扫描:mode=1 找最低位 1(Find First one),mode=0 找最低位 0,找不到返回 -1。用于遍历 mask / 检测连续到达 | L1244 |
2.3 内存层级与线程协作的三个关键机制
机制一:stride 循环(线程步进遍历)
1024 个线程要处理 BS×K 个元素(可能远多于 1024),标准写法是"每个线程从自己的 tid 出发,每次跨线程总数前进一步":
// simt_prepare_mapping 内,L139:
for (int row = tid; row < axisBS*axisK; row += PREPARE_THREADS_NUM) {
// 线程 0 处理 row=0,1024,2048...;线程 1 处理 row=1,1025...
}
机制二:UB 原子计数器 = "取号机"
多个线程同时要往同一个专家的"槽位数组"里写数据,谁写第 0 槽、谁写第 1 槽?atomicAdd 返回旧值,天然就是一个并发取号机:
// L143-148:原子加 1 并拿到"我是第几个到达的"(pre_cnt)
int pre_cnt = atomicAdd(dstExpCnt + cur_ep_id, 1);
dstExpOffsetList[cur_ep_id*axisBS + pre_cnt] = row; // 按取到的号入座,绝不冲突
机制三:栅栏 + 软同步
- 核内:
ThreadBarrier()保证所有线程完成第一阶段(计数/写入)后才开始第二阶段(排序/消费)。 - 核间:AIV 之间没有硬件全局栅栏,本算子用"在 GM 状态区写标志 + 轮询求和"实现软同步(见 §5.8 的 flag+cnt 协议、§4.5 的 stageDone)。
DataCacheCleanAndInvalid 清缓存(否则可能永远读到旧值,见 L1689 WaitDispatch),这是本算子中反复出现的套路。3. 算子设计思路
3.1 通信背景:Full-Mesh 与通信窗(Window)
"Full-Mesh" 指每张卡与所有 EP(Expert Parallel)域内的卡都建立了一条直连通道。发送方不需要"经过中转",直接把数据写到对方卡的接收窗口里。每张卡有一块 shmemBuffer_(共享内存窗口),并被切分成若干功能区(L462 GetWindAddrByRankId 及其周边函数):
3.2 多核角色分工:一条 AIV 流水线
算子运行时有 aivNum_ 个 AIV 核,代码没有让所有核干同一件事,而是把核按编号切段,各司其职(角色划分计算见 L773 SetTilingDataAndCal,调度见 L4278 Process):
3.3 生产者-消费者流水线
整个算子是一条精心设计的异步流水线:Stage 核只管"打包发货清单 + 把 token 放进发货架",URMA 核负责"填写运单(WQE)",Doorbell 核负责"按顺序按门铃通知运货车",CumSum 核负责"对账(各专家收了多少)",最后所有核作为收件方把自己专家的数据按到达顺序上架(连续化)。它们之间几乎不互相等待,靠窗口里的标志位协议衔接。
4. 核心算法流程
4.1 总体流程图
4.2 阶段一:Token 本地缓存(Stage 核 → send buffer)
- 专家 ID 预处理(ExpIdsCopyAndMaskCal):把
expertIds(BS×K 的矩阵)搬进 UB,若存在 token 掩码 / 专家掩码 / 零计算专家,用CompareScalar + GatherMask生成筛选掩码,无效位置填 -1。这一步保证后续"每个有效 (token, topk) 对"恰好被发送一次。 - 按 token 分核(SendToMoeExpertNew):每个 Stage 核认领一段 token(
SplitToCore均分 + 余数前置),把每个 token 只写一次到本地 send buffer 第tokenId格。 - 打包(TokenToExpertInQuant / TokenToExpert):可选量化,然后
FillTriple在数据尾部写入三元组(epRankId, tokenIndex, topkIndex)以及完整的 top-k expert id 列表(L1015-1028),再按 512B 块铺到 send buffer。 - 完工广播(PublishStageDone):置自己核的 stageDone 标志 = 1.0。
4.3 阶段二:URMA 远程发送(WQE 核)
- 建映射:VF
simt_prepare_mapping(1024 线程)扫全部 expertIds,用 UB 原子计数把"目标专家在本核负责区间内"的 (token, 顺序) 记进桶里,并统计要发给远端的 token 总数。 - 等绿灯:轮询 headReady 标志(L3772),确认 Doorbell 核已读完 SQ 头指针,避免并发改写竞争。
- 造运单:VF
buildRemoteWqeDesc(128 线程 = 4 warp,每 warp 负责一个"专家×槽位段")计算每条转发的 (本地地址, 远端地址, 目标卡, 长度, 操作码) 五元组,写入 UB 上的 WQE 描述数组。 - 写入硬件队列:VF
simt_nw_mj_vf三阶段把运单变成硬件可消费的 64B WQE,写入各卡对应的 SQ(Send Queue)环形缓冲区。
4.4 阶段三:敲门铃(Doorbell 核)
WQE 写完只是"把信投进邮筒",还要"摇晃通知旗"硬件才开始搬运——这就是 doorbell(门铃)。由 1 个专用核(RingDoorbell)轮询每个 jetty(每卡 1 条通道)的 headRecord 位图,发现连续就绪的 WQE 后批量 st_dev(currHead, dbAddr) 通知硬件。它还负责前置工作:从 ACLSHMEM 拷贝 SQ/CQ 上下文(get_sqs_and_cqs_xb)并清缓存直写。
4.5 阶段四:计数与 CumSum(对账)
CumSum 核组(≤16 核,CalCumSum)完成"收发对账":
- 报数:CalAndSendCnt 统计"本卡会发给每个专家多少 token",以 flag+cnt 格式写入各目标卡状态区;
- 对账:WaitDispatch 自旋等待所有源卡报数完成(flag 全为 1),随后 GatherSumRecvCnt + GetCumSum 做跨核前缀和(并行前缀:核 i 得到核 0..i-1 的累计收件数),结果写
sendCountsOut与 workspace; - 输出专家 token 数:一个核执行 SetExpertTokenNums,对各专家跨所有源卡求和。
4.6 阶段五:接收整理(所有核 → LocalWindowCopy)
最后所有核汇合执行 LocalWindowCopy:每个核认领若干 (源卡×本地专家) 数据块,轮询块的到达标志(§5.9),到多少搬多少,把窗口里的"按源卡分块"数据重排成"按专家连续排列"的 expandXOut,同时写 expandIdxOut,最后清理标志供下一轮复用。这是典型的"工作窃取/轮询调度":数据没到就跳去看下一个块,先到先搬,最大化吞吐。
5. 关键代码逻辑解析
5.1 常量与全局布局(先记住这几个数字)
| 常量 | 值 | 含义 |
|---|---|---|
SPLIT_BLOCK_SIZE / SPLIT_BLOCK_DATA_SIZE | 512 / 480 B | 传输的最小单元:512B 中 480B 放数据,末尾 32B 中的第一个 float 是到达标志(=1.0) |
STATE_OFFSET | 32 B | 状态区最小格:float flag + int32 cnt + 冗余对齐 |
PREPARE_THREADS_NUM / WQE_THREADS_NUM / WARP_SIZE | 1024 / 128 / 32 | 各 VF 的线程数(编译期常量,必须与 Dim3{} 一致) |
MAX_WQE_CNT | 128 | 一次 VF 调用最多积累的 WQE 条数(UB 描述数组容量) |
headReadyFlag | 20260817 | 魔数(日期戳),WQE 核与 Doorbell 核的握手暗号 |
BUFFER_NUM / dataState_ | 2 | 窗口双缓冲,两轮 dispatch 交替使用前后半区 |
5.2 simt_prepare_mapping:原子计数 + 桶内排序
L122-246。输入是 expertIds,输出是"我负责的每个专家收到的 token 索引列表(按 token 序号升序)"。两阶段:
// 阶段 1:扫描全部 BS×K 个槽位(stride 循环)
for (int row = tid; row < axisBS*axisK; row += PREPARE_THREADS_NUM) {
int ep_id = *(expertIds + row);
if (ep_id >= beginExpertId && ep_id < expertNum + beginExpertId) {
int cur_ep_id = ep_id - beginExpertId; // 换算成本核区间内编号
int pre_cnt = atomicAdd(dstExpCnt + cur_ep_id, 1); // 取号:我是该专家第 pre_cnt 个 token
dstExpOffsetList[cur_ep_id*axisBS + pre_cnt] = row; // 入桶(桶宽 axisBS)
}
}
Simt::ThreadBarrier(); // 全员到齐才开始阶段 2(排序要读完整桶)
// 阶段 2:对每个桶元素,数一数桶里比它小的元素个数 = 它的升序位置(计数排序思想)
for (uint32_t t = tid; t < expertNum * axisBS; t += PREPARE_THREADS_NUM) {
...
for (uint32_t c = 0; c < offsetNum; ++c)
if (dstExpOffsetList[curExpertSlotBase + c] < curTokenOriginalOffset)
sortedSlotInExpert++;
dstExpOrderedOffsetList[curExpertSlotBase + sortedSlotInExpert] = curTokenOriginalOffset;
}
5.3 buildRemoteWqeDesc:warp 协作生成运单
L250-299。128 线程 = 4 warp,每轮(loopIdx)每个 warp 认领一个"专家×32 槽"块:
const uint32_t tid = Simt::GetThreadIdx<0>();
const uint32_t warpId = tid / WARP_SIZE, laneId = tid % WARP_SIZE;
// 本 warp 本轮负责的 (专家, 第几批32槽)
uint32_t currBlock = (BUILD_THREADS_NUM / WARP_SIZE) * loopIdx + warpId;
uint32_t mySlot = currBatchOfCurrExp * WARP_SIZE + laneId; // 本线程检查的槽位
if (targetRankId != localRankId && mySlot < validOffsetCnt) { // 只处理远端 & 有效槽
uint32_t taskOffset = atomicAdd(finishedWqeDescNum, 1); // 取号:运单序号
localAddrList[taskOffset] = srcBaseAddr + tokenId * tokenCommSize; // 源:send buffer 第 tokenId 格
remoteWqeAddrList[taskOffset] = remoteBaseAddr + (targetExpertOnRank * axisBS + mySlot) * tokenCommSize; // 目的:远端窗口槽位
dstRankList[taskOffset] = targetRankId; messageLenList[taskOffset] = tokenCommSize;
}
远端窗口基址 + (本地专家在对方卡上的编号 × axisMaxBS + 该专家内第 mySlot 个 token) × hCommuSize。这正是接收端 LocalWindowCopy 按块扫描的布局——发送端定槽位,接收端就能"按图索骥"。5.4 Token 发送:512B 块布局与三元组
TokenToExpertInQuant 展示了"一个 token 如何被铺成若干 512B 块":blockCntPerToken_ = Ceil(hOutSizeAlign_, 480),每块 480B 数据 + 32B(其中 flag float 占位)。UB 内用 Copy 指令以 256B 为段、跨步 16/15 双段拷贝,把数据填入 outTensor_ 的两个半区(flagPadOffset_ 在 0/256 间乒乓),再整块 DataCopy 到 GM。
5.5 simt_nw_mj_vf:WQE 三阶段写入(性能精髓)
L3277-3407。SQ(发送队列)是每条 jetty 一个环形缓冲,多线程并发写环形队列必须解决"队列头指针 headAddr 在 GM 上、每次 atomicAdd 很贵"的问题。解法是三阶段聚合:
| 阶段 | 做什么 | 性能意义 |
|---|---|---|
| Phase 1 路由计数 | 每线程领一条运单,atomicAdd(dstRankWqeCount[dstRank]) 得到"它是发往该卡的第几条",映射到 SQ,并 atomicAdd(sqWqeCount[sq]) 统计每条 SQ 本批数量 | 原子操作全部落在 UB(快) |
| Phase 2 批量占头 | 每条非空 SQ 由一个线程执行一次 GM atomicAdd:sqBaseHead[sq] = atomicAdd(headAddr, count) | 把 GM 原子次数从 每 WQE 一次降到 每活跃 SQ 每批一次 |
| Phase 3 写入 + 记录 | 每线程取回自己的运单,atomicAdd(sqCursor[sq]) 在预留区间内领偏移,调 simt_write_wqe_v4 写 64B WQE;asc_threadfence() 后用 atomicOr 在 UB 位图(HeadRecordUB)中标记该槽就绪 | 写序保证 + 位图供 Doorbell 核快速扫描 |
// Phase 2(L3334-3341):核心技巧
for (int sqIdx = tid; sqIdx < total_jetty; sqIdx += WQE_THREADS_NUM) {
uint32_t count = sqWqeCount[sqIdx];
if (count > 0U)
sqBaseHead[sqIdx] = atomicAdd((__gm__ uint32_t *)(sqs[sqIdx].headAddr), count);
// 一次预留 count 个连续槽位,Phase 3 在区间内自由分配
}
// Phase 3(L3344-3361):写入 + 栅栏 + 置位图
...
simt_write_wqe_v4(my_sq, my_head, ...);
asc_threadfence(); // 确保 WQE 先于位图可见
atomicOr(&HeadRecordUB[...], 1U << bitInWord); // 标记第 my_head % maxHeadRecordSlot 槽就绪
最后(L3365-3378)按 warp 把 UB 位图中非零 word atomicOr 合并到 GM 的 HeadRecord——Doorbell 核读的就是它。
5.6 simt_write_wqe_v4:64B WQE 位域布局
L3031-3092。WQE(Work Queue Element)是 URMA 硬件的"运单",固定 64B,两个 ulonglong4(各 32B)写入。关键位域:
| 字段 | 位置 | 含义 |
|---|---|---|
| owner | Word0 bit31 | 环形队列翻转位:(myHead & qpDepth)==0 ? 1 : 0,硬件据此区分"新写的"与"上一圈残留的" WQE |
| flag / tokenEn / rmtJettyType | Word0 bit16-29 | 描述符有效性、token 使能、远端 jetty 类型 |
| opcode | Word0 bit40+ | 本算子恒为 OP_WRITE(远程写) |
| tpId / sgeNum | Word1 | 传输端口 ID;SGE(分散聚合元素)个数=1 |
| rmtObjId / rmtEid | Word1、Word2-3 | 远端 jetty 标识 / 远端端点 ID(16 字节,来自 ACLSHMEM 上下文) |
| rmtAddr | Word10-11 (dw[5]) | 远端目的地址(接收窗口槽位) |
| SGE: len / laddr | dw[6] / dw[7] | 传输长度 hCommuSize / 本地源地址(send buffer) |
5.7 RingDoorbell:位图扫描与敲门
L4001-4275。为什么单独一个核干这事?因为 SIMT 线程无法正确地直接敲门铃(源码注释 L3381 明确说明),于是 WQE 核把就绪信息写进位图,由 Doorbell 核(Scalar 核属性)串行确认后敲:
- 初始化:读各 SQ 当前 head 作 prevHead(防竞争基线);
set_gm_valueVF 清零 headRecord 区;向各 WQE 核的 headReady 槽写魔数20260817(L4096),放行 WQE 生成。 - 主循环(L4165):对每张有欠账的远端卡,按 32 位 word 批量拉取位图,用
ScalarGetSFFValue<0>统计从 prevHead 起连续置位的个数 readyCount(半 word 特判右移,L4200-4209),然后st_dev(prevHead + wqeCount, dbAddr)一次敲响 wqeCount 个 WQE 的门铃。 - 终止条件:
ranksLeftToSend == 0(calc_rank_token_cnt 预先算好每卡需发总数)。
st_dev(昂贵的设备寄存器写)次数压缩了一个数量级。5.8 计数与跨核前缀和:flag+cnt 协议与软同步
这是全文件最精巧的核间同步设计。状态区每个 32B 格:第 0 个 int32 按 float 解释是 flag(发完置 0x3F800000=1.0f),第 1 个 int32 是 cnt(L1460-1463 用带掩码的 Duplicate 一次铺出这种"每 8 个元素第 1 个是 1.0"的模板)。
值得注意的细节:每次轮询前都要 DataCacheCleanAndInvalid<..., CACHELINE_OUT>(L1689)——AIV 的 DCache 不与远端写自动同步,不刷缓存会死循环读旧值。
5.9 到达检测与输出整理
CheckDataArriveWithFlag —— 一块一块地"验货"
L1922-2049。对某源块中 copyCnt 个 token(每 token 含 blockCntPerToken_ 个 512B 子块):
// 1) 把每个子块尾部的 flag(float) 拉到 UB:DataCopy 跨步参数 {1, 512-32B ...} 天然"跳格"采集
DataCopy(flagRecvTensor_, dataFlagGlobal[token * tokenStride], flagCopyParams);
// 2) GatherMask 抽出所有 flag → CompareScalar(flag==1.0) 得位图
GatherMask(flagGatherOutTensor_, flagRecvTensor_, flagRecvGatherMask_, ...);
CompareScalar(flagCompResultU8_, flagGatherOutTensor_, float(1), CMPMODE::EQ, compareCount);
// 3) ScalarGetSFFValue<0> 找位图中第一个 0(= 第一个未到的子块)
// 前面连续为 1 的子块数 / blockCntPerToken_ = 已完整到达的 token 数
return arriveFlagNum / blockCntPerToken_;
WaitAndFormatOutput / CopyInAndOut —— 轮询式收件 + top-k 修正
WaitAndFormatOutput 以"(index+1)%validNum 轮询"在多个源块间跳转:哪块到得多先搬哪块;一次最多搬 maxCopyTokenCnt 个 token(受 UB 容量约束,L2107)。CopyInAndOut 整块 DataCopy 到 UB 后:
// L2076-2093:URMA 转发时三元组里的 topkIdx 被统一填成 0,
// 但 token 携带了完整 top-k expert id 列表 —— 在这里现场"验明正身"
for (uint32_t j = 0; j < axisK_; j++) {
int32_t curExpertId = xOutInt32Tensor(topKInfoOffset + j);
if (dstExpertId == curExpertId) {
xOutInt32Tensor(tokenQuantAlign_ + 2) = j; // 修正为真实 topk slot
break;
}
}
DataCopyPad(expandIdxGMTensor_[dstPosition * EXPAND_IDX_INFO], xOutInt32Tensor[...], ...);
搬完后若整块收完,还要把窗口里的 flag 清零(L2162-2170),配合 dataState_ 双缓冲,为下一轮 dispatch 复位现场。
5.10 Process:总调度
if ASCEND_IS_AIV {
if (aivId_ < aivUsedStage_) { // ── 发件组
AllToAllDispatch(); // 打包 token → send buffer / 共享专家直发
PublishStageDone(); // 完工标志
tpipe_->Reset();
if (aivId_ < aivUsedRemoteWqe_) URMASendToken(); // 远端:建映射+造 WQE
else SelfCopyToken(); // 本卡:等 stage 全完成直接拷
} else if (aivId_ < aivCumSumStart_) {
RingDoorbell(); // ── 敲门组(1 核)
} else {
CalCumSum(); // ── 对账组(≤16 核)
}
PipeBarrier<PIPE_ALL>();
LocalWindowCopy(); // ── 所有核:收件整理(本卡作为收件方)
}
LocalWindowCopy 在收自己的快递。全 mesh 网中所有卡对称运行,这就是为什么窗口布局、flag 协议必须严格对称。6. 内存布局速查(收发双方约定)
| 区域 | 发送方视角 | 接收方视角 |
|---|---|---|
| ① StageDone(每核 512B×axisBS) | PublishStageDone 置 1.0 | SelfCopy/Doorbell 核 WaitAllStageDone 求和等待(L874) |
| ② Token send buffer(每 token hCommuSize) | TokenToExpert 系列写入 | —(仅发送侧使用;SelfCopy 的源) |
| ③ URMA 控制(headReady/headRecord/SQ/CQ) | WQE 核写 SQ + 置位图 | Doorbell 核扫位图、敲门铃;硬件消费 SQ |
| ④ 状态区(flag+cnt,每格 32B) | CalAndSendCnt 写远端 | WaitDispatch / GatherSumRecvCnt 读本地 |
| ⑤ 数据区(每 源卡×本地专家 expertPerSizeOnWin) | URMA 硬件按 WQE 写远端 | CheckDataArriveWithFlag 验货 → CopyInAndOut 出货 |
7. 性能优化策略盘点
| # | 策略 | 代码证据 | 原理 |
|---|---|---|---|
| 1 | 每 token 只量化一次,多目标复用 | SendToMoeExpertNew + URMA 二次转发 | 量化计算 BS 次而非 BS×K 次,转发放到通信硬件 |
| 2 | 原子操作分层:UB 优先,GM 批量化 | Phase 2 批量占头 | GM atomicAdd 每活跃 SQ 每批仅一次,摊薄跨核原子开销 |
| 3 | 位图 + 位扫描代替逐项轮询 | HeadRecord atomicOr;ScalarGetSFFValue 扫前缀 | 一次 64 位扫描判定 64 个子块状态;门铃批量敲 |
| 4 | 多级软同步解耦 | StageDone / headReady / flag+cnt / cumsumFlag 四套独立协议 | 各角色只在必要的点上握手,其余全异步流水 |
| 5 | warp 级分工 | buildRemoteWqeDesc 每 warp 认领一个专家槽位段 | warp 内 32 线程天然并行且无锁,atomicAdd 仅在 UB |
| 6 | 双缓冲 dataState 乒乓 | L952-955 | 窗口与状态区各分两半,消除轮次间清理依赖(配合显式清 flag 兜底) |
| 7 | 向量化批量标志采集 | CheckDataArriveWithFlag 跳格 DataCopy + GatherMask | 一次 MTE 搬运采集多块 flag,避免标量逐个读 GM |
| 8 | 缓存直写/失效 | DataCacheCleanAndInvalid、cacheWriteThrough | 跨核/跨卡数据可见性正确性保障(SQ 上下文还需直写穿透) |
| 9 | UB 复用与 tpipe_ 反复 Reset | CalCumSum 内 Reset、LocalWindowCopy 内 Reset | 各阶段 UB 需求不同,按阶段重规划,190KB 上限内精细分配 |
| 10 | 轮询调度 + 批量搬运 | WaitAndFormatOutput 主循环 | 先到先服务,单次搬 maxCopyTokenCnt 个 token,摊薄固定开销 |
7.1 效果评估与代价分析
| 策略 | 定量/定性收益 | 代价与风险 |
|---|---|---|
| 每 token 只量化一次(策略 1) | 量化计算次数从 BS×K 降为 BS(Top-K=8 时约 8 倍收益);量化由向量核完成,转发交给 URMA 硬件与计算重叠 | WQE 描述符与转发路径复杂度上升;需携带 top-k id 列表并做接收侧修正 |
| GM 原子批量化(策略 2) | SQ 头指针的 GM atomicAdd 从 O(WQE 数) 降为 O(活跃 SQ 数 × 批数);一批 128 条 WQE 通常只需个位数次跨核原子 | 三阶段逻辑引入两次核内栅栏;占头与写入之间需保证时序 |
| 位图 + 位扫描(策略 3) | 门铃(st_dev 设备写)次数降低约一个数量级;64 位扫描一次判定 64 个槽位,判定复杂度 O(1)/slot | headRecord 位图区域需要额外 GM 空间与清理开销(RingDoorbell 内 set_gm_value 清零) |
| 四级软同步(策略 4) | 各角色仅在必要点握手,等待时间大部分被 URMA 传输与量化计算重叠掉 | 无硬件栅栏兜底,协议正确性完全依赖 flag 语义与缓存清理;调试难度高 |
| warp 级分工(策略 5) | WQE 生成的原子操作全部落在 UB,延迟比 GM 原子低 1-2 个数量级;128 线程吞吐填满 128 条/批描述数组 | 批容量 MAX_WQE_CNT=128 限制单轮批量,末批可能不满 |
| 双缓冲(策略 6) | 轮次间无需全局清空数据区,只需清各自的 flag,两轮 dispatch 可流水衔接 | 窗口内存占用翻倍(数据区与状态区各分两半) |
| 512B 块布局(§5.4) | 以块为粒度的到达检测,使"收多少搬多少"的增量处理成为可能 | 有效载荷率 480/512 = 93.75%,约 6.25% 带宽用于 flag 与对齐 |
| 轮询调度收件(策略 10) | 消除"等最慢源块"的队头阻塞,整体收件时间 ≈ max(各源块到达时间) 而非 Σ(串行等待) | 轮询本身消耗标量/MTE 事务;validNum 很大时 index 遍历开销上升 |
8. 潜在改进方向
- 桶内排序 O(n²) → O(n log n) 或 O(n):
simt_prepare_mapping阶段 2 的比较计数排序在单专家 token 数很大时代价高。可用 warp 级 bitonic 排序,或直接利用"取号即到达序"的性质改为按 (row) 排序的基数思路(源码中已保留 warp 协作版注释 L217-245,说明作者已探索过)。 - 轮询自旋的能效:
WaitDispatch、headReady 等自旋循环在高频场景会烧核(含 DCache 清理开销)。可引入带退避的轮询间隔,或硬件事件(如有)替代。 - 魔法数字与重复代码收敛:WQE 写入函数 5 个版本并存、大量被注释掉的调试块(如 L1951-2006)、
DEBUG_ON/DEBUG_CLOCK_ON调试宏混布,建议抽公共模板并用constexpr常量表管理位域。 - PORT_NUM/JETTY_NUM 目前为 1:L106-107,多 jetty 可提升单卡聚合带宽与 WQE 并行度,三阶段路由代码已为其预留(
(writerId + ordinal) % jetty_per_rank),值得展开验证。 - URMAPollCQ 超时机制未完成:L3474 处 300 万 cycle 超时仅打印即 break,可能掩盖故障;建议计入失败统计并上抛。
- 可移植性:
#if __NPU_ARCH__ == 3510分支较多,位域打包依赖make_ulonglong4;若要跨代移植,需把 WQE 布局抽象成版本化 trait。 - _recv 前缀和核数上限 16(
CUMSUM_MAX_CORE_NUM)在大规模专家数下每核状态段变长,轮询拉取量增大,可考虑树形归约替代"全量广播 + 局部 GatherMask"。
9. 算子接口规范(输入 / 输出)
9.1 核函数入口与 Init 参数
算子以模板类 MoeDistributeDispatchV2FullMesh(L349)组织,Host 侧完成 Tiling 后调用 Init 绑定全部资源,再调用 Process 执行(每个 AIV 核各跑一次,aivId_ = GetBlockIdx())。Init 签名(L958-962)即算子的完整接口契约:
__aicore__ inline void Init(
GM_ADDR mc2Context, // MC2 共享上下文(含各卡 epHcclBuffer / shmem 基址)
GM_ADDR x, // 输入 token 隐藏状态
GM_ADDR expertIds, // Top-K 专家 ID 矩阵
GM_ADDR scales, // 平滑 scale(可选)
GM_ADDR xActiveMask, // token/专家 掩码(可选)
GM_ADDR elasticInfo, // 弹性缩容信息(可选)
GM_ADDR performanceInfo, // 性能打点输出(isPerformance 时)
GM_ADDR expandXOut, // 输出:按专家连续排列的 token 数据
GM_ADDR dynamicScalesOut, // 输出:动态量化 scale
GM_ADDR expandIdxOut, // 输出:路由三元组
GM_ADDR expertTokenNumsOut, // 输出:每专家 token 数
GM_ADDR sendCountsOut, // 输出:累计收件数前缀和
GM_ADDR tpSendCountsOut, // 输出:tp 域计数(兼作耗时打点区)
GM_ADDR workspaceGM, TPipe *pipe, // 中间 workspace 与 UB 管道
const MoeDistributeDispatchV2TilingData *tilingData); // Host 预计算参数包
9.2 输入张量详表
| 张量 | 数据类型 | 形状 | 说明与约束 |
|---|---|---|---|
x | XInType(XType 模板;3510 架构下 fp4x2 系列自动降为 uint8 搬运,L352-366) | [BS, H] | 本卡 token 隐藏状态;DataCopyPad 按 copyInAxisH_ × sizeof(XInType) 整行搬入(L1006) |
expertIds | int32 | [BS, K] | Top-K 专家编号;值 -1、越界、被 xActiveMask 剔除或属于零计算量专家的槽位不发送(-1 填充见 L2454) |
scales | float | [BS, scalesCol] | 平滑 scale;IsSmoothScaleExist 时按 scaleInBytes_ 拼接到传输块尾部(L1084-1089) |
xActiveMask | bool | [BS](isTokenMask)或 [BS, K](isExpertMask) | token 掩码只影响计数(activeMaskBsCnt_);专家掩码用 GatherMask 生成 validExpertIdsTensor_/有效索引(L2280-2365) |
elasticInfo | int32 | [4 + 2×epWorldSize] | 弹性缩容:[0]=isScalingDown, [1]=epWorldSize, [2]=sharedExpertRankNum, [3]=moeExpertNum, [4..]=rank 重映射表(读取逻辑 L724-741);使能后所有 toRankId 查表换算 |
mc2Context | Mc2MoeContext* | — | isMc2Context 时替代 HCCL 上下文;窗口地址经 aclshmem_ptr(shmemBuffer_, rank) 换算(L474-478) |
tilingData | 结构体 | — | Host 侧 Tiling 产物,字段清单见 §9.4 |
9.3 输出张量详表
| 张量 | 数据类型 | 形状 | 写出位置 | 语义 |
|---|---|---|---|---|
expandXOut | ExpandXOutType | [Σ expertTokenNum, H'] | CopyInAndOut L2073 | 接收到的 token,按本地专家分组、组内按 (源卡前缀和 + 到达序) 连续排列,可直接作为专家 GEMM 输入 |
dynamicScalesOut | uint8(承载 fp8 scale) | 同 expandX 行数 × scaleOutBytes | L2072 CopyScalesToOut | 每行动态量化 scale,从传输块 scale 段抽出 |
expandIdxOut | int32 | [BS×K, 3] | L2095 | 每输出行的 (srcRankId, tokenIndex, topkIndex) 三元组;topkIndex 由携带的 top-k id 列表现场还原(L2076-2093) |
expertTokenNumsOut | int64 | [本地专家数] | SetExpertTokenNums L1795 | 每本地专家收到的 token 总数;expertTokenNumsType_=0 输出累计值、=1 输出独立值(L1788-1792) |
sendCountsOut | int32 | [rscvStatusNum] | L1751 | 跨核前缀和后的累计收件数(单调不减),接收侧据此把每个源块映射到 expandX 的连续目标区间 |
tpSendCountsOut | int32 | — | — | tp 域发送计数;DEBUG_CLOCK_ON 时被复用为耗时打点输出区(L4336-4351) |
9.4 Tiling 参数(Host 传入)
以下字段全部来自 SetTilingData(L744-770)与 Init 中的补充读取(L969-987),是 Host 侧 Tiling 阶段必须正确填写的参数面:
| 字段 | 类型 | 含义 |
|---|---|---|
| bs / h / k / globalBs | uint32 | 本卡 batch size、隐藏维度、Top-K、全局 batch(用于 axisMaxBS_ = globalBs/epWorldSize) |
| epWorldSize / epRankId | uint32 | EP 域卡数与本卡 rank |
| sharedExpertRankNum / sharedExpertNum / moeExpertNum | uint32 | 共享专家卡数 / 共享专家数 / MoE 专家总数;派生 moeExpertNumPerRank_ = moeExpertNum / (epWorldSize - sharedExpertRankNum) |
| aivNum | uint32 | SetBlockDim 的核数,决定角色切分(§3-2) |
| scalesCol / scalesTypeSize / scalesCount | uint32 | scale 列数 / 元素字节数 / 计数,合成 scaleInBytes_ |
| isTokenMask / isExpertMask / zeroComputeExpertNum | uint32/bool | 掩码模式与零计算量专家数(决定 ExpIdsCopyAndMaskCal 分支) |
| expertTokenNumsType / hasElasticInfo / isPerformance | uint32/bool | 输出语义开关(§9.3) |
| maxSizeForUbBuffer | uint32 | UB 临时缓冲上限(量化/掩码分支的 Buffer 规划依据) |
| totalWinSizeEp / isMc2Context / shmemBuffer | uint64/bool | EP 通信窗总大小、上下文模式、shmem 基址(非 MC2 模式经 CheckWindowSize 校验,L976-977) |
9.5 量化模式与数据类型组合
| QuantMode | 行为 | 代码证据 |
|---|---|---|
UNQUANT | 若 ExpandXOutType == XType 直接整块转发;类型不同则经 Cast 转换 | L1166、L1052-1055(3510 非量化直转 hifp8) |
PERTOKEN_DYNAMIC_QUANT | 每 token 动态 scale,输出写入 dynamicScalesOut | L1389 |
MX_QUANT / PERGROUP_DYNAMIC_QUANT | 按 256B 组计算 scale,搬入前需 Duplicate 填 0 清脏数据 | L1040-1042 |
(QuantMode > UNQUANT) || (QuantMode == UNQUANT && ExpandXOutType != XType) 判定是否走 TokenToExpertInQuant(L1166 / L1261 / L1309)——即"显式量化模式或隐式类型转换需求"都会触发量化路径。10. 算子生命周期总览
10.1 五个生命周期阶段
10.2 Init 初始化详解
Init(L958-1012) 按严格顺序完成 7 件事:
- 饱和模式设置:3510 架构关闭浮点饱和(L964-966),避免量化 Cast 溢出钳位;
- 上下文选择:isMc2Context 时使用 Mc2MoeContext,否则取 HCCL 上下文并
CheckWindowSize校验通信窗容量(L970-978); - 张量绑定:为 10 个 GlobalTensor 调用 SetGlobalBuffer(L979-986);
- 参数解析:SetTilingDataAndCal(§10.3);
- 标志清理:
ClearStageDoneFlags(L834)把本卡窗口 StageDone 区清零,保证本轮"完工广播"从干净状态开始; - 数据状态初始化:
SetDataStatus(L924)读取/翻转dataState_双缓冲,并据此计算shmemDataSizeOffset_ / winDataSizeOffset_(L952-955),把窗口切到本轮使用的半区; - 窗口地址族初始化:计算 windowGM_ / statusDataSpaceGM_ / recvCntWorkspaceGM_ 及 DataCopy 参数(L995-1008)。
10.3 参数解析与派生量计算
SetTilingDataAndCal(L773-831)是"参数 → 布局"的推导中枢,关键派生链:
| 派生量 | 公式(代码语义) | 用途 |
|---|---|---|
moeExpertNumPerRank_ | moeExpertNum / moeExpertRankNum | 专家→rank 换算的分母 |
expertIdsCnt_ | axisBS × axisK | 路由矩阵元素总数 |
hAlignSize_ / hOutSizeAlign_ | 量化对齐宽 + expandInfoSize = UB_ALIGN + Ceil(K×4, UB)×UB | token 携带三元组 + top-k id 列表后的传输宽度(L802-806) |
blockCntPerToken_ / hCommuSize_ | ⌈hOutSizeAlign_/480⌉;× 512 | 单 token 的传输块数与总传输字节数 |
expertPerSizeOnWin_ | axisMaxBS × hCommuSize | 窗口中每个 (源卡 × 本地专家) 数据块的尺寸 |
rscvStatusNum_ | 共享专家卡:epWorldSize;MoE 卡:epWorldSize × moeExpertNumPerRank | 状态区格子总数 / sendCountsOut 长度 |
aivUsedCumSum_ | min(⌊专家总数/16⌋, ⌊aivNum/2⌋, 16, rscvStatusNum),下限 1(L815-819) | CumSum 核数 |
aivCumSumStart_ / aivUsedStage_ | aivNum - aivUsedCumSum;其值 - aivUsedDoorbell_(1) | 角色分界(图 3-2) |
sharedUsedAivNum_ / moeUsedAivNum_ / aivUsedRemoteWqe_ | 按 (K+共享专家数) 比例切分;RemoteWqe = aivUsedStage_ - aivUsedSelfCopy_(4) | 发件组内二次分工 |
totalJettyNum | epWorldSize × PORT_NUM(1) × JETTY_NUM(1) | SQ/CQ 上下文条目数 |
11. 核心算法逻辑架构与数学原理
11.1 问题形式化
设本卡 token 为 x_i ∈ R^H(i ∈ [0,BS)),路由矩阵 E ∈ Z^{BS×K},E[i][j] = e 表示 token i 的第 j 个激活专家编号。定义映射:
σ : (i, j) ──▶ ( 目标卡 r(e), 本地专家 e_local = e % moeExpertNumPerRank, 组内槽位 s )
算子的目标是:对每个本地专家 e_local,把"全 EP 域所有发往它的 token"按确定的顺序(源卡优先、卡内按 token 序)连续拼成 expandX 的行段,且同时给出逆映射 expandIdx。形式化地,输出行位置由两级前缀和唯一确定:
dstPosition(源块 src) = sendCountsOut[src - 1] + 该块内已收完成数
11.2 发送槽位寻址公式
发送侧的核心是"把每个 (i,j) 对号入座到远端窗口的某个 512B 槽位"。三种路径的寻址公式:
| 路径 | 远端槽位地址公式 | 代码对应 |
|---|---|---|
| 共享专家(SIMD 直发) | Wind(r) + expertPerSizeOnWin × epRankId + slotIdx × hCommuSize,slotIdx = 有效 token 序(先验已知 = activeMaskBsCnt) | L1160-1161 |
| MoE(旧 SIMD 直发路径) | Wind(r) + expertPerSizeOnWin × (epRankId × moeExpertNumPerRank + e_local) + dstTokenPreCnt × hCommuSize | L1256-1258 |
| MoE(主路径:send buffer + URMA 转发) | 先 send buffer[tokenId](本地);再 remoteBase(r) + (e_local × axisBS + mySlot) × hCommuSize,其中 mySlot = 该 token 在"发往专家 e 的升序 token 序列"中的位次 | L3747、buildRemoteWqeDesc L290-292 |
Win[r][e_local][slot](每元素 hCommuSize 字节)的线性化寻址。发送方填 mySlot(升序保证唯一性),接收方按 (r, e_local) 整块扫描——寻址在发送端一次性确定,接收端零协商,这是全 mesh 无锁设计能够成立的前提。11.3 接收定位与跨核前缀和
接收侧两个定位量:
- 源块号反解(L2132):
expertMapTensor_存储复合索引src = e_local × epWorldSize + srcRank,反解srcDataBlockIdx = (src % epWorldSize) × localExpertNum + src / epWorldSize,即窗口中"该源卡发给该本地专家"的数据块首地址偏移; - 目标行定位(L2151-2152):
dstPosition = sendCntTensor_[src-1] + expertFinishNumTensor_[index]——前者是 CumSum 核组算出的"排在 src 之前的所有块的总收件数"(全局前缀),后者是本核在 src 块内已收的行数(局部计数)。
跨核前缀和由 CumSum 核组协作完成(GetCumSum L1612-1668):核 k 收到全部核的 cnt 广播后,对 [0, k) 求和即得自己的全局偏移——本质是把 O(N²) 的两两交换压缩为"一轮全广播 + 一轮局部 GatherMask/Sum"。
11.4 到达判定原理
定义块级指示函数 flag(t, b) ∈ {0, 1}(token t 的第 b 个 512B 子块尾部的 float)。则:
token t 已完整到达 ⟺ ∀ b < blockCntPerToken : flag(t, b) = 1
可收 token 数 = |{ 最大前缀 t : flag(0..t, ·) 全 1 }| = (连续置位 flag 总数) / blockCntPerToken
该判定的正确性依赖三个不变量:(1) 发送端"先数据后 flag"的写序(硬件 DMA 顺序 + 栅栏);(2) 接收端读前 DataCacheCleanAndInvalid 的可见性;(3) 到达的前缀有序性(由发送端升序填槽 + WQE 按序写 + 门铃按序敲共同保证)。三者任一被破坏,接收端要么死等要么收到半块脏数据——这也是 §7.1 总评中提到的协议脆弱点。
12. 处理阶段划分与执行顺序(时序图)
下图为一次 dispatch 中本卡各角色与远端卡、URMA 硬件之间的消息时序。三条泳道并行推进(发送链 / 计数链 / 通信链),仅在标注的握手点同步:
阶段执行顺序约束表
| 顺序约束 | 保证机制 | 代码位置 |
|---|---|---|
| 量化打包 → 远程转发 | stageDone 全员置位后 SelfCopy/URMA 才取数(WaitAllStageDone) | L874、L3964 |
| prevHead 快照 → WQE 可写 | headReady 魔数握手(WQE 核等 Doorbell 核读完 SQ head) | L3772 / L4096 |
| WQE 写入 → 门铃 | asc_threadfence + headRecord 位图(先数据后置位) | L3355-3360 |
| 数据到达 → 收件 | 块尾 flag=1.0 前缀判定 | L1922 |
| 对账完成 → expandX 定位 | sendCounts 前缀和就绪后才允许写输出(CalRecvAndSetFlag 内软同步) | L1729 |
13. 关键步骤与代码对应关系
| # | 步骤 | 函数 / VF | 行号 | 实现机制关键词 |
|---|---|---|---|---|
| 1 | 参数解析与派生 | SetTilingData / SetTilingDataAndCal | L744/L773 | tiling → 布局常量推导链(§10.3) |
| 2 | 初始化 | Init | L958 | 上下文选择、张量绑定、清标志、dataState 选区 |
| 3 | 窗口状态初始化 | SetDataStatus / ClearStageDoneFlags | L924/L834 | 双缓冲半区计算(L952-955) |
| 4 | 专家 ID 预处理 | ExpIdsCopyAndMaskCal | L2430 | CompareScalar + GatherMask 筛选、零计算专家剪枝 |
| 5 | token 本地缓存 | SendToMoeExpertNew / TokenToExpertInQuant / FillTriple | L1279/L1031/L1015 | 按 token 分核、512B 块、三元组 + top-k id 列表 |
| 6 | 共享专家直发 | SendToSharedExpert | L1140 | 共享专家组内 rank 定位、SIMD 直写远端窗口 |
| 7 | 完工广播 | PublishStageDone / WaitAllStageDone | L853/L874 | flag 求和轮询 |
| 8 | 专家→token 桶映射 | simt_prepare_mapping (VF) | L122 | UB 原子取号 + 计数排序 |
| 9 | 运单生成 | buildRemoteWqeDesc (VF) | L250 | warp 分工、五元组描述数组 |
| 10 | WQE 入队 | simt_nw_mj_vf / simt_write_wqe_v4 | L3277/L3031 | 三阶段路由、批量占头、64B 位域 |
| 11 | 敲门铃 | RingDoorbell | L4001 | headRecord 位图扫描、st_dev 批量推进 |
| 12 | 本卡专家拷贝 | SelfCopyToken | L3892 | WaitAllStageDone 后 UB 中转直拷 |
| 13 | 计数与对账 | CalAndSendCnt / WaitDispatch / GatherSumRecvCnt | L1450/L1671/L1574 | flag+cnt 协议、DCache 失效、ReduceSum |
| 14 | 跨核前缀和 | GetCumSum / CalRecvAndSetFlag / SetExpertTokenNums | L1612/L1729/L1767 | 全广播 + 局部 GatherMask/Sum |
| 15 | 收件整理 | LocalWindowCopy / CheckDataArriveWithFlag / CopyInAndOut | L2196/L1922/L2052 | 位图验货、轮询调度、topkIndex 修正 |
| 16 | 复位与打点 | WaitDispatchClearStatus / 清块 flag / DebugClock 上抛 | L1559/L2162/L4336 | 为下一轮 dataState 翻转做好准备 |
14. 上下游模块接口与数据交互
接口交互协议清单
| 交互对象 | 传递内容 | 交互方式 / 同步 |
|---|---|---|
| Host Tiling | tilingData 参数包(§9.4)、aivNum(SetBlockDim)、UB 上限(SetLocalMemorySize) | Kernel 启动参数,一次性传入,无运行时交互 |
| MC2 / HCCL 上下文 | 各卡窗口基址(epHcclBuffer_ / GetHcclContext)、rank 世界信息 | Init 阶段只读解析(L970-978) |
| ACLSHMEM 库 | shmemBuffer 基址、aclshmem_ptr 远端指针换算、UDMA QP 上下文(sqPtr/scqPtr/memPtr) | 设备侧 API 调用(L2524);数据可见性由 cacheWriteThrough 保证 |
| URMA 硬件 | 64B WQE(SQ 环)、doorbell(head 推进值)、CQ 轮询 | 写 SQ + st_dev 敲铃异步触发;完成情况可经 URMAPollCQ 轮询 CQE(L3459) |
| 远端卡(对等 Kernel) | token 数据块 + flag(⑤ 区)、flag+cnt 状态(④ 区)、headRecord(③ 区) | URMA 远程写 + 双方约定的软同步协议(§5.8、§11.4) |
| 下游专家计算 | expandXOut / dynamicScalesOut / expertTokenNumsOut | 标准张量输出,由上层调度在 dispatch 完成后消费 |
| Combine 算子 | expandIdxOut 三元组(逆映射依据)、相同的窗口基础设施 | dispatch 输出即 Combine 输入,窗口 flag 协议对称复用 |
deepep_moe_dis_dispatch.h 为准;该文件的 SHA-256 为 e2e87f115784687ea8c91b370183001ab7a17198203faa38e530263536da96fc 。15. 术语表
- AIV / AI Core
- 昇腾 AI 处理器上的向量计算核;本算子
SetBlockDim启动的每个"核"即一个 AIV,GetBlockIdx()得到 aivId。 - SIMT / SIMD
- 单指令多线程 / 单指令多数据;前者线程级并行(本算子),后者向量级并行。
- VF(Vector Function)
__simt_vf__标记的核内线程函数,通过Simt::VF_CALL以 N 线程启动。- LAUNCH_BOUND(N)
- 声明 VF 线程数的编译期常量宏,必须与
Dim3{N}一致。 - Warp / lane
- 32 个 SIMT 线程的执行单元;lane 为 warp 内线程号(tid%32)。
- UB(Unified Buffer)
- 核内高速缓存,同核线程以
__ubuf__指针共享;容量约 190-216KB 可用。 - GM(Global Memory)
- 全局显存(HBM),
__gm__指针访问,跨核跨卡可见。 - EP(Expert Parallel)
- 专家并行:把 MoE 的专家切分到多卡。
- Top-K / axisK
- 每个 token 激活的专家数;expertIds 形状为 BS×K。
- Window(通信窗)
- 每卡注册的一块可被其他卡远程读写的共享内存区(§3-1 图)。
- URMA
- AI 侧远程内存访问引擎:AI Core 写 WQE + 敲门铃即可触发跨卡 DMA,不经 Host。
- WQE(Work Queue Element)
- URMA 的 64B 工作描述符,含远端地址/本地地址/长度/操作码等。
- SQ / CQ / jetty
- 发送队列 / 完成队列 / 通信通道实例;本算子每卡 1 port × 1 jetty。
- Doorbell(门铃)
- 向硬件寄存器写"队列头推进值"以触发处理,
st_dev实现。 - flag+cnt 协议
- 32B 状态格中 float 标志位(1.0=就绪)+ int32 计数,本算子的核心同步原语。
- 软同步
- 无硬件全局栅栏时,用"写标志 → 轮询求和 → 清零"在核间达成同步的方式。
- dataState / 双缓冲
- 窗口数据分两半交替使用,轮次 r 用 dataState=r&1,避免读写交叉。
- expandX / expandIdx
- 输出:按专家连续排列的 token 数据 / 每输出行的 (rank, tokenIdx, topkIdx) 三元组。
- CumSum(前缀和)
- sendCountsOut 是"各源卡累计收件数"的单调序列,用于把源块映射到 expandX 的连续区间。
- ScalarGetSFFValue
- 位扫描 intrinsic:找最低位 1(<1>)或最低位 0(<0>),返回 -1 表示无。
- DataCacheCleanAndInvalid
- 清并失效 DCache 行,强制后续读穿透到 GM——读远端更新前的必备动作。
- st_dev
- 向设备侧(非普通内存)地址写值的指令,用于敲硬件门铃。
- headReadyFlag = 20260817
- WQE 核与 Doorbell 核的握手魔数(日期戳)。
16. 附录:调试手段与延伸阅读
16.1 内置维测设施
| 设施 | 位置 | 用途 |
|---|---|---|
DebugClock(i) + timePoint[16] | L19-25、L4336-4351 | 各阶段耗时打点(cycle 差),最终写入 tpSendCountsOut 区供主机侧读取 |
AscendC::Simt::printf / AscendC::printf | 多处(已注释) | 设备侧打印;排查竞态时注意打印本身会改变时序(Heisenbug) |
RunPosRecord(runPos) | L2185 | 把执行阶段编号写入状态区第 1 格,用于死后定位卡点阶段 |
RecordRankCommDuration | L1801 | 性能模式(isPerformanceFlag_)下记录每源卡通信耗时(取 max,atomicAdd int32 低 32 位写法见注释) |
16.2 与本项目的关系
本头文件位于参考实现目录 ref/,本项目 ascend_deepep 的对应内核在 kernels/dispatch_udma.cpp,接口层在 csrc/ops/dispatch.cpp。对照阅读时可重点关注:send buffer 槽位映射、WQE 三阶段写入、flag+cnt 同步协议是否在自研版本中保留/演进。
16.3 推荐阅读顺序(针对初学者)
本文档基于源码静态分析生成(2026-08-31);行号引用以本次提供的 deepep_moe_dis_dispatch.h 为准。